module GeoRuby
  module SimpleFeatures
    # Raised when an error in the EWKB string is detected
    class EWKBFormatError < StandardError
    end

    # Parses EWKB strings and notifies of events (such as the beginning of the definition of geometry, the value of the SRID...) the factory passed as argument to the constructor.
    #
    # =Example
    # factory = GeometryFactory::new
    # ewkb_parser = EWKBParser::new(factory)
    # ewkb_parser.parse(<EWKB String>)
    # geometry = @factory.geometry
    #
    # You can also use directly the static method Geometry.from_ewkb
    class EWKBParser
      def initialize(factory)
        @factory = factory
      end

      # Parses the ewkb string passed as argument and notifies the factory of events
      def parse(ewkb)
        @factory.reset
        @with_z = false
        @with_m = false
        @unpack_structure = UnpackStructure.new(ewkb)
        parse_geometry
        @unpack_structure.done
        @srid = nil
      end

      private

      def parse_geometry
        @unpack_structure.endianness = @unpack_structure.read_byte
        geometry_type = @unpack_structure.read_uint

        if (geometry_type & Z_MASK) != 0
          @with_z = true
          geometry_type = geometry_type & ~Z_MASK
        end
        if (geometry_type & M_MASK) != 0
          @with_m = true
          geometry_type = geometry_type & ~M_MASK
        end
        if (geometry_type & SRID_MASK) != 0
          @srid = @unpack_structure.read_uint
          geometry_type = geometry_type & ~SRID_MASK
        else
          # to manage multi geometries : the srid is not present in sub_geometries, therefore we take the srid of the parent ; if it is the root, we take the default srid
          @srid ||= DEFAULT_SRID
        end

        case geometry_type
        when 1
          parse_point
        when 2
          parse_line_string
        when 3
          parse_polygon
        when 4
          parse_multi_point
        when 5
          parse_multi_line_string
        when 6
          parse_multi_polygon
        when 7
          parse_geometry_collection
        else
          fail EWKBFormatError.new('Unknown geometry type')
        end
      end

      def parse_geometry_collection
        parse_multi_geometries(GeometryCollection)
      end

      def parse_multi_polygon
        parse_multi_geometries(MultiPolygon)
      end

      def parse_multi_line_string
        parse_multi_geometries(MultiLineString)
      end

      def parse_multi_point
        parse_multi_geometries(MultiPoint)
      end

      def parse_multi_geometries(geometry_type)
        @factory.begin_geometry(geometry_type, @srid)
        num_geometries = @unpack_structure.read_uint
        1.upto(num_geometries) { parse_geometry }
        @factory.end_geometry(@with_z, @with_m)
      end

      def parse_polygon
        @factory.begin_geometry(Polygon, @srid)
        num_linear_rings = @unpack_structure.read_uint
        1.upto(num_linear_rings) { parse_linear_ring }
        @factory.end_geometry(@with_z, @with_m)
      end

      def parse_linear_ring
        parse_point_list(LinearRing)
      end

      def parse_line_string
        parse_point_list(LineString)
      end

      # used to parse line_strings and linear_rings
      def parse_point_list(geometry_type)
        @factory.begin_geometry(geometry_type, @srid)
        num_points = @unpack_structure.read_uint
        1.upto(num_points) { parse_point }
        @factory.end_geometry(@with_z, @with_m)
      end

      def parse_point
        @factory.begin_geometry(Point, @srid)
        x, y = *@unpack_structure.read_point
        if ! (@with_z || @with_m) # most common case probably
          @factory.add_point_x_y(x, y)
        elsif @with_m && @with_z
          z = @unpack_structure.read_double
          m = @unpack_structure.read_double
          @factory.add_point_x_y_z_m(x, y, z, m)
        elsif @with_z
          z = @unpack_structure.read_double
          @factory.add_point_x_y_z(x, y, z)
        else
          m = @unpack_structure.read_double
          @factory.add_point_x_y_m(x, y, m)
        end

        @factory.end_geometry(@with_z, @with_m)
      end
    end

    # Parses HexEWKB strings. In reality, it just transforms the HexEWKB string into the equivalent EWKB string and lets the EWKBParser do the actual parsing.
    class HexEWKBParser < EWKBParser
      # parses an HexEWKB string
      def parse(hexewkb)
        super(decode_hex(hexewkb))
      end

      # transforms a HexEWKB string into an EWKB string
      def decode_hex(hexewkb)
        [hexewkb].pack('H*')
      end
    end

    class UnpackStructure #:nodoc:
      NDR = 1
      XDR = 0

      def initialize(ewkb)
        @position = 0
        @ewkb = ewkb
      end

      def done
        fail EWKBFormatError.new('Trailing data') if @position != @ewkb.length
      end

      def read_point
        i = @position
        @position += 16
        fail EWKBFormatError.new('Truncated data') if @ewkb.length < @position
        @ewkb.unpack("@#{i}#{@double_mark}#{@double_mark}@*")
      end

      def read_double
        i = @position
        @position += 8
        fail EWKBFormatError.new('Truncated data') if @ewkb.length < @position
        @ewkb.unpack("@#{i}#{@double_mark}@*").first
      end

      def read_uint
        i = @position
        @position += 4
        fail EWKBFormatError.new('Truncated data') if @ewkb.length < @position
        @ewkb.unpack("@#{i}#{@uint_mark}@*").first
      end

      def read_byte
        i = @position
        @position += 1
        fail EWKBFormatError.new('Truncated data') if @ewkb.length < @position
        @ewkb.unpack("@#{i}C@*").first
      end

      def endianness=(byte_order)
        if (byte_order == NDR)
          @uint_mark = 'V'
          @double_mark = 'E'
        elsif (byte_order == XDR)
          @uint_mark = 'N'
          @double_mark = 'G'
        end
      end
    end
  end
end
